const Code = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const File = std.fs.File;
const Parser = @import("Parser.zig");
const CodeMap = std.StaticStringMap([]const u8);

ramaddr: usize = 15,
buffer: [16]u8 = @splat(0),
parser: Parser,

pub fn init(allocator: Allocator, asmfile: File) !Code {
    return .{
        .parser = try .parse(allocator, asmfile),
    };
}

pub fn deinit(self: *Code) void {
    self.parser.deinit();
}

pub fn compile(c: *Code, hack: File) !void {
    // write to hack file
    for (c.parser.statements.items) |statement| {
        try hack.writer().print("{s}\n", .{try c.translate(statement)});
    }
}

fn translate(c: *Code, statement: []const u8) ![]const u8 {
    if (statement[0] == '@') {
        // A instruction
        const lastidx = for (statement, 0..) |char, i| {
            switch (char) {
                ' ', '/' => {
                    break i;
                },
                else => {},
            }
        } else statement.len;
        const token = statement[1..lastidx];

        const num = switch (statement[1]) {
            '0'...'9' => blk: {
                break :blk try std.fmt.parseInt(usize, token, 0);
            },
            else => blk: {
                var value = c.parser.symtable.get(token);
                if (value == null) {
                    c.ramaddr += 1;
                    try c.parser.symtable.put(token, c.ramaddr);
                    value = c.ramaddr;
                }
                break :blk value;
            },
        };

        c.write("0{b:0>15}", .{num.?});
    } else {
        // C instruction
        var it = std.mem.tokenizeAny(u8, statement, "=; /");
        const has_dest = if (std.mem.indexOfScalar(u8, statement, '=') != null) true else false;
        const has_jump = if (std.mem.indexOfScalar(u8, statement, ';') != null) true else false;

        var dest: []const u8 = "";
        var comp: []const u8 = "";
        var jump: []const u8 = "";
        var dest_code: []const u8 = "000";
        var comp_code: []const u8 = undefined;
        var jump_code: []const u8 = "000";

        if (has_dest) {
            dest = it.next().?;
            dest_code = map.dest.get(dest).?;
        }

        comp = it.next().?;
        comp_code = map.comp.get(comp).?;

        if (has_jump) {
            jump = it.next().?;
            jump_code = map.jump.get(jump).?;
        }

        c.write("111{s}{s}{s}", .{ comp_code, dest_code, jump_code });
    }

    return &c.buffer;
}

fn write(c: *Code, comptime format: []const u8, args: anytype) void {
    _ = std.fmt.bufPrint(&c.buffer, format, args) catch unreachable;
}

const map = struct {
    const dest: CodeMap = .initComptime(.{
        .{ "M", "001" },
        .{ "D", "010" },
        .{ "DM", "011" },
        .{ "MD", "011" },
        .{ "A", "100" },
        .{ "AM", "101" },
        .{ "MA", "101" },
        .{ "AD", "110" },
        .{ "DA", "110" },
        .{ "ADM", "111" },
        .{ "AMD", "111" },
        .{ "MAD", "111" },
        .{ "MDA", "111" },
        .{ "DMA", "111" },
        .{ "DAM", "111" },
    });

    const comp: CodeMap = .initComptime(.{
        .{ "0", "0101010" },
        .{ "1", "0111111" },
        .{ "-1", "0111010" },
        .{ "D", "0001100" },
        .{ "A", "0110000" },
        .{ "M", "1110000" },
        .{ "!D", "0001101" },
        .{ "!A", "0110001" },
        .{ "!M", "1110001" },
        .{ "-D", "0001111" },
        .{ "-A", "0110011" },
        .{ "-M", "1110011" },
        .{ "D+1", "0011111" },
        .{ "A+1", "0110111" },
        .{ "M+1", "1110111" },
        .{ "D-1", "0001110" },
        .{ "A-1", "0110010" },
        .{ "M-1", "1110010" },
        .{ "D+A", "0000010" },
        .{ "D+M", "1000010" },
        .{ "D-A", "0010011" },
        .{ "D-M", "1010011" },
        .{ "A-D", "0000111" },
        .{ "M-D", "1000111" },
        .{ "D&A", "0000000" },
        .{ "D&M", "1000000" },
        .{ "D|A", "0010101" },
        .{ "D|M", "1010101" },
    });

    const jump: CodeMap = .initComptime(.{
        .{ "JGT", "001" },
        .{ "JEQ", "010" },
        .{ "JGE", "011" },
        .{ "JLT", "100" },
        .{ "JNE", "101" },
        .{ "JLE", "110" },
        .{ "JMP", "111" },
    });
};
